﻿using System;
using System.Collections.Generic;
using System.Linq;
using FakeItEasy;
using NUnit.Framework;

namespace NJabber.Client
{
    [TestFixture]
    public class RosterCacheTest
    {
        private RosterCache rosterCache;

        [SetUp]
        public void Setup()
        {
            rosterCache = new RosterCache(A.Fake<IRosterSynchronizer>());
        }

        [Test]
        [ExpectedException(typeof(ArgumentNullException))]
        public void Constructor_NullParameter_ThrowsException()
        {
            RosterCache cache = new RosterCache(null);
        }

        #region ChangedItems tests

        [Test]
        public void ChangedItems_CacheContainsChangedItems_ReturnsChangedItems()
        {
            rosterCache.Reset(Enumerable.Range(0, 3).Select(i => new RosterItem("", string.Format("item{0}@jid.net", i))));
            
            RosterItem changedItem1 = rosterCache["item0@jid.net"];
            RosterItem changedItem2 = rosterCache["item2@jid.net"];
            RosterItem unchangedItem = rosterCache["item1@jid.net"];

            changedItem1.Name = "item0";
            changedItem2.Name = "item2";

            Assert.That(rosterCache.ChangedItems.Count(), Is.EqualTo(2));
            CollectionAssert.Contains(rosterCache.ChangedItems, changedItem1);
            CollectionAssert.Contains(rosterCache.ChangedItems, changedItem2);
            CollectionAssert.DoesNotContain(rosterCache.ChangedItems, unchangedItem);
        }

        [Test]
        public void ChangedItems_CacheContainsChangedItems_AddedItemsAndRemovedItemsAreEmpty()
        {
            rosterCache.Reset(Enumerable.Range(0, 3).Select(i => new RosterItem("", string.Format("item{0}@jid.net", i))));

            rosterCache["item0@jid.net"].Name = "item0";
            rosterCache["item2@jid.net"].Name = "item2";
            
            CollectionAssert.IsEmpty(rosterCache.AddedItems);
            CollectionAssert.IsEmpty(rosterCache.RemovedItems);
        }

        [Test]
        public void ChangedItems_CacheContainsChangedAndAddedItems_ReturnsChangedItems()
        {
            rosterCache.Reset(Enumerable.Range(0, 3).Select(i => new RosterItem("", string.Format("item{0}@jid.net", i))));

            RosterItem changedItem = rosterCache["item0@jid.net"];
            rosterCache.Add(new RosterItem("new_item", "new_item@jid.com"));

            changedItem.Name = "item0";

            Assert.That(rosterCache.ChangedItems.Count(), Is.EqualTo(1));
            CollectionAssert.Contains(rosterCache.ChangedItems, changedItem);
        }

        [Test]
        public void ChangedItems_CacheContainsChangedAndRemovedItems_ReturnsChangedItems()
        {
            rosterCache.Reset(Enumerable.Range(0, 3).Select(i => new RosterItem("", string.Format("item{0}@jid.net", i))));

            RosterItem changedItem = rosterCache["item0@jid.net"];
            rosterCache.Remove("item1@jid.net");

            changedItem.Name = "item0";

            Assert.That(rosterCache.ChangedItems.Count(), Is.EqualTo(1));
            CollectionAssert.Contains(rosterCache.ChangedItems, changedItem);
        }

        #endregion

        #region Indexer tests

        [Test]
        public void Item_CacheContainsAddedItem_ReturnsItem()
        {
            RosterItem item = new RosterItem("item-a", "amy@lee.com");
            rosterCache.Add(item);

            RosterItem actualItem = rosterCache[item.Jid];

            Assert.That(actualItem, Is.EqualTo(item));
        }

        [Test]
        public void Item_CacheContainsPushedItem_ReturnsItem()
        {
            RosterItem item = new RosterItem("item-a", "amy@lee.com");
            rosterCache.Push(item);

            RosterItem actualItem = rosterCache[item.Jid];

            Assert.That(actualItem, Is.EqualTo(item));
        }

        [Test]
        [ExpectedException(typeof(InvalidOperationException))]
        public void Item_CacheDoesNotContainsItem_ThrowsException()
        {
            RosterItem item = rosterCache["unexisted@jid.com"];
        }

        [Test]
        [ExpectedException(typeof(ArgumentNullException))]
        public void Item_NullParameter_ThrowsException()
        {
            RosterItem item = rosterCache[null];
        }

        #endregion

        #region Add method tests

        [Test]
        public void Add_NotNullRosterItem_ItemsContainsItem()
        {
            RosterItem item = new RosterItem("item8", "some@jid.com");
            
            rosterCache.Add(item);

            CollectionAssert.Contains(rosterCache.Items, item);
            CollectionAssert.Contains(rosterCache.AddedItems, item);
        }

        [Test]
        [ExpectedException(typeof(ArgumentNullException))]
        public void Add_NullParameter_ThrowsException()
        {
            rosterCache.Add(null);
        }

        #endregion

        #region AcceptItemChanges method tests

        [Test]
        public void AcceptItemChanges_HasAddedItems_RemovesFromAddedItems()
        {
            RosterItem item = new RosterItem("roster-item", "item@roster.com");
            rosterCache.Add(item);

            rosterCache.AcceptItemChanges(item.Jid);

            CollectionAssert.Contains(rosterCache.Items, item);
            CollectionAssert.DoesNotContain(rosterCache.AddedItems, item);
        }

        [Test]
        public void AcceptItemChanges_HasRemovedItems_RemovesFromRemovedItems()
        {
            RosterItem item = new RosterItem("roster-item", "some@jid.com");
            rosterCache.Push(item);
            rosterCache.Remove(item.Jid);

            rosterCache.AcceptItemChanges(item.Jid);

            CollectionAssert.DoesNotContain(rosterCache.Items, item);
            CollectionAssert.DoesNotContain(rosterCache.RemovedItems, item);
        }

        [Test]
        [ExpectedException(typeof(ArgumentNullException))]
        public void AcceptItemChanges_NullParameter_ThrowsException()
        {
            rosterCache.AcceptItemChanges(null);
        }

        #endregion

        #region DiscardItemChanges method tests

        [Test]
        public void DiscardItemChanges_HasAddedItems_RemovesItemFromAddedItemAndItems()
        {
            RosterItem item = new RosterItem("some-item", "some@item.org");
            rosterCache.Add(item);

            rosterCache.DiscardItemChanges(item.Jid);

            CollectionAssert.DoesNotContain(rosterCache.Items, item);
            CollectionAssert.DoesNotContain(rosterCache.AddedItems, item);
        }

        [Test]
        [ExpectedException(typeof(ArgumentNullException))]
        public void DiscardItemChanges_NullParameter_ThrowsException()
        {
            rosterCache.DiscardItemChanges(null);
        }

        [Test]
        public void DiscardItemChanges_HasRemovedItems_RemovesItemFromRemovedItems()
        {
            RosterItem item = new RosterItem("some-item", "some@item.org");
            rosterCache.Push(item);
            rosterCache.Remove(item.Jid);

            rosterCache.DiscardItemChanges(item.Jid);

            CollectionAssert.DoesNotContain(rosterCache.RemovedItems, item);
        }

        #endregion

        #region Remove method tests

        [Test]
        public void Remove_ItemsContainItemWithGivenJid_RemoveItem()
        {
            RosterItem item = new RosterItem("item", "my@jid.com");
            rosterCache.Push(item);

            rosterCache.Remove(item.Jid);

            CollectionAssert.DoesNotContain(rosterCache.Items, item);
            CollectionAssert.Contains(rosterCache.RemovedItems, item);
        }

        [Test]
        [ExpectedException(typeof(ArgumentNullException))]
        public void Remove_NullParameter_ThrowsException()
        {
            rosterCache.Remove(null);
        }

        [Test]
        [ExpectedException(typeof(InvalidOperationException))]
        public void Remove_ItemsDoesNotContainItemWithGivenJid_ThrowsException()
        {
            rosterCache.Add(new RosterItem("item-1", "amt@domain.com"));
            rosterCache.Remove("invalid@jid.com");
        }

        #endregion

        #region Push method tests

        [Test]
        public void Push_NotNullRosterItem_ItemNotExistsInAddedOrRemovedItems()
        {
            RosterItem item = new RosterItem("roster-item", "roster@item.vor");

            rosterCache.Push(item);

            CollectionAssert.DoesNotContain(rosterCache.AddedItems, item);
            CollectionAssert.DoesNotContain(rosterCache.RemovedItems, item);
            CollectionAssert.Contains(rosterCache.Items, item);
        }

        [Test]
        [ExpectedException(typeof(ArgumentNullException))]
        public void Push_NullParameter_ThrowsException()
        {
            rosterCache.Add(null);
        }

        #endregion

        #region Reset method tests

        [Test]
        public void Reset_NoArguments_ClearPreviousRoster()
        {
            IEnumerable<RosterItem> rosterItems1 = GenerateRosterItems();
            RosterCache manager = CreateRosterCache();
            foreach (var item in rosterItems1) manager.Add(item);

            manager.Reset();

            CollectionAssert.IsEmpty(manager.Items);
        }

        [Test]
        public void Reset_RosterCollectionArgument_ClearPreviousRosterAndSetNewRoster()
        {
            IEnumerable<RosterItem> newRoster = GenerateRosterItems(3, 4, 5);
            RosterCache manager = CreateRosterCache(0, 1, 2);

            manager.Reset(newRoster);

            CollectionAssert.AreEquivalent(newRoster, manager.Items);
        }

        #endregion

        [Test]
        public void Invalidate_RosterChanged_RestoresPreviousState()
        {
            RosterCache cache = CreateRosterCache(1, 2, 3);
            IEnumerable<RosterItem> expectedRoster = GenerateRosterItems(1, 2, 3);
            ChangeItemsInCache(cache);

            cache.Invalidate();

            VerifySubmitedChanges(cache, expectedRoster);
        }

        private static void VerifySubmitedChanges(RosterCache manager, IEnumerable<RosterItem> expectedRoster)
        {
            CollectionAssert.IsEmpty(manager.AddedItems);
            CollectionAssert.IsEmpty(manager.RemovedItems);
            CollectionAssert.AreEquivalent(expectedRoster, manager.Items);
        }

        private void ChangeItemsInCache(RosterCache manager)
        {
            RosterItem itemToAdd = GenerateRosterItems(4).Single();
            RosterItem itemToRemove = GenerateRosterItems(1).Single();
            manager.Add(itemToAdd);
            manager.Remove(itemToRemove.Jid);
        }

        private RosterCache CreateRosterCache(params int[] itemsIndices)
        {
            IEnumerable<RosterItem> initialItems = GenerateRosterItems(itemsIndices);
            RosterCache cache = new RosterCache(A.Fake<IRosterSynchronizer>());
            cache.Reset(initialItems);
            return cache;
        }

        private IEnumerable<RosterItem> GenerateRosterItems(params int[] indices)
        {
            if (indices.Length > 0) return indices.Select(i => new RosterItem("item" + i, "jid" + i + "@domain.com"));
            return Enumerable.Range(1, 3).Select(i => new RosterItem("item" + i, "jid" + i + "@domain.com"));
        }

        [Test]
        public void Sync_AnyRosterItems_CallsMethodSyncOfSynchronizer()
        {
            IRosterSynchronizer synchronizer = A.Fake<IRosterSynchronizer>();
            rosterCache = new RosterCache(synchronizer);

            rosterCache.Sync();

            A.CallTo(() => synchronizer.Sync(rosterCache.Changes)).MustHaveHappened();
        }
    }
}
